Solution: The Model Has an Active Record
Let's solve the antipattern Magic Beans using Active Record.
Controllers handle application input while views handle application output, both relatively simple and well-defined tasks. Frameworks are best at helping us put these together quickly. But it’s hard for a framework to provide a one-size-fits-all solution for models because models comprise the rest of the object-oriented design for our application.
This is where we actually need to identify what the objects are in our application and what data and behavior those objects have. It’s true what Robert L. Glass said:the majority of software development is intellectual and creative.
Grasping the model#
Fortunately, there’s a lot of wisdom in the field of object-oriented design to guide us. Craig Larman’s book Applying UML and Patterns describes guidelines called the General Responsibility Assignment Software Patterns (GRASP). Some of these guidelines are especially relevant to separating models from their data access objects:
Information expert#
The object responsible for an operation should have all the data needed to fulfill that operation. Since some operations in our application involve multiple tables (or no tables) and Active Record is good at working with only one table at a time, we need another class to aggregate several database access objects together and use them for the composite operation.
The relationship between a model and a Data Access Object like Active Record should be HAS-A (aggregation) instead of IS-A (inheritance). Most frameworks that rely on Active Record assume the IS-A solution. If our model uses Data Access Objects instead of inheriting from the Data Access Object class, then we can design the model to contain all the data and code for the domain that it’s supposed to model — even if it takes multiple database tables to represent it.
Creator#
How the model persists its data in a database should be an internal implementation detail. A domain model that aggregates its Data Access Objects should have the responsibility to create those objects.
The controllers and views in our application should use the domain model interface without being aware of what kind of database interaction is necessary for the model to fetch or store data. This makes it easy to change the database queries later in one place in our application.
Low coupling#
It’s important to decouple logically independent blocks of code. This gives us the flexibility to change the implementation of a class without affecting its consumers. We can’t simplify the requirements of the application; some complexity has to reside somewhere in our code. But we can make the best choice about where we implement that complexity.
High cohesion#
The interface for the domain model class should reflect its intended usage, not the physical database structure or CRUD operations. Generic methods of the Active Record interface like find()
, first()
, insert()
, and even save()
don’t tell us much about how they apply to application requirements. Methods like assignUser()
are more descriptive, and our controller code is much easier to understand.
When we decouple a model class from the Data Access Object it uses, we can even design more than one model class for the same Data Access Object. This is better for cohesion than trying to combine all work related to the given tables into a single class and extending Active Record.
Putting the domain model into action#
In Domain-Driven Design: Tackling Complexity in the Heart of Software, Eric Evans describes a better solution: the “domain model”.
A model in the original MVC sense — not in the fallacious software sense — is an object-oriented representation of a domain in our application, that is, the business rules in our application and the data for those business rules. The model is where we implement business logic for the application; storing it in a database is an internal implementation detail of a model.
Once we have the model designed around concepts in our application, we can start implementing database operations that are completely hidden within our model classes instead of database layout. Let’s look at a possible refactoring of our earlier example code:
<?php
class BugReport
{
protected $bugsTable;
protected $accountsTable;
protected $productsTable;
public function __construct()
{
$this->bugsTable = Doctrine_Core::getTable("Bugs");
$this->accountsTable = Doctrine_Core::getTable("Accounts");
$this->productsTable = Doctrine_Core::getTable("Products");
}
public function create($summary,
$description, $reportedBy)
{
$bug = new Bugs();
$bug->summary = $summary
$bug->description = $description
$bug->status = "NEW";
$bug->reported_by = $reportedBy;
$bug->save();
}
public function assignUser($bugId, $assignedTo)
{
$bug = $bugsTable->find($bugId);
$bug->assigned_to = $assignedTo;
$bug->save();
}
public function get($bugId)
{
return $bugsTable->find($bugId);
}
public function search($status, $searchString)
{
$q = Doctrine_Query::create()
->from("Bugs b")
->join("b.Products p")
->where("b.status = ?", $status)
->andWhere("MATCH(b.summary, b.description) AGAINST (?)", $searchString]);
return $q->fetchArray();
}
}
class AdminController extends Zend_Controller_Action
{
public function assignAction()
{
$this->bugReport->assignUser(
$this->_getParam("bug"),
$this->_getParam("user"));
}
}
class BugController extends Zend_Controller_Action
{
public function enterAction()
{
$auth = Zend_Auth::getInstance();
if ($auth && $auth->hasIdentity()) {
$identity = $auth->getIdentity();
}
$this->bugReport->create(
$this->_getParam("summary"),
$this->_getParam("description"),
$identity);
}
public function displayAction()
{
$this->view->bug = $this->bugReport->get(
$this->_getParam("bug"));
}
}
class SearchController extends Zend_Controller_Action
{
public function bugsAction()
{
$this->view->searchResults = $this->bugReport->search(
$this->_getParam("status", "OPEN"),
$this->_getParam("search"));
}
}
?>
It’s easy to notice several improvements:
- The class interaction diagram shown below is much simpler and easier to read, indicating an improvement in decoupling classes.
-
By decoupling the model’s interface from its underlying database structure, we’ve reduced and simplified the code in the controller.
-
Each model class creates the objects to interact with one or more tables. The controllers do not need to know which tables are involved.
-
The model classes encapsulate and hide the database queries. The controller only needs to be concerned with retrieving user inputs and invoking higher-level tasks through the model API.
-
In some cases, a query is too complex to run easily through a Data Access Object and writing custom SQL is needed. Using plain SQL seems less scary when it’s safely encapsulated inside a model class.
Testing plain objects#
Ideally, we should be able to test our model without connecting to a live database. If we decouple our model from its Data Access Object, we can create “stub” and “mock” Data Access Objects to help unit test our model.
Likewise, we can test the interface of a domain model just like any other object-oriented testing: by calling methods of the object and then validating the method’s return value. This is faster and easier than creating fake HTTP requests to feed to a controller and parsing the resulting HTTP response.
We still test our controllers with fake HTTP requests, but because the controller code is simpler, we don’t need to test as many logical paths.
If we separate models and controllers and separate data access components from models, we can unit test all these classes more simply and with better isolation. This makes it easier to diagnose defects when they occur. Isn’t this the point of unit tests?
Using your new skills#
We can use a data access object productively in any software development framework, even one that encourages the Magic Beans antipattern. However, developers who don’t learn how to employ object-oriented design principles are doomed to write spaghetti code.
The basics of domain modeling described and cited in this chapter will help us choose the best design to support testing and code maintenance. We’ll finally be able to achieve outstanding productivity developing database-driven applications.